Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

efi: change how HostEnvironment overrides EFI variables #312

Conversation

chrisccoulson
Copy link
Collaborator

@chrisccoulson chrisccoulson commented Jun 21, 2024

The HostEnvironment interface contains a ReadVar method for abstracting
reads of EFI variables. The default environment just calls
efi.ReadVariable, with other implementations providing mocked variables.

The github.com/canonical/go-efilib package provides a lot more APIs now
that read from (and some that write to) EFI variables and some of these
will be used from the new efi/preinstall package (these APIs are mostly
boot and secure boot related). I wanted a way to be able to override the
environment for the preinstall.RunChecks call (particularly as this is
very useful for testing), but the existing HostEnvironment approach doesn't
work well for this.

I initially considered a couple of options:

  • Make it possible to mock each of the individual calls to go-efilib
    functions via the HostEnvironment interface.
  • Export varsBackend from go-efilib and provide a function in to mock
    the backend by overriding the system one.

The first approach scales poorly, so my initial approach was going to be
the second one. The problem with this though is that HostEnvironment
will be part of a public, production API, and mocking the backend is global
to the whole process. I don't think this is appropriate in production code
where it might be possible that there may be one goroutine reading EFI
variables from the system when another goroutine decides to mock the backend
based on a supplied HostEnvironment.

I've taken a different approach instead, by modifying every go-efilib
function that interacts with EFI variables to accept a context.Context
argument. The context contains a VarsBackend keyed from it, and the package
exports a DefaultVarContext symbol corresponding to the default system
backend (efivarfs if it is detected, or a null backend if it isn't
detected). I've used a context rather than passing the VarsBackend directly
because the default efivarfs backend may make use of the optional
cancellation and deadline during writing, as it runs a retry loop that potentially
races with other applications.

This approach permits individual call sites to override the backend to
use for individual functions that interact with EFI variables (not just
ReadVariable, WriteVariable and ListVariables, but there's a whole set
of extra functions related to secure boot and boot configuration that
make use of reading and writing EFI variables).

The ReadVar method on the HostEnvironment interface has gone, and a new
method that returns a context has been added. The internal.DefaultEnv
implementation just returns efi.DefaultVarContext.

The MockVars type in internal/efitest has been updated to implement
efi.VarsBackend, and the MockHostEnvironment implementation in
internal/efitest returns a context that keys to this implementation.

This should hopefully allow preinstall.RunChecks to be able to take a
HostEnvironment argument and benefit from mocked variables even when
interacting with variables indirectly using higher level functions
exported from go-efilib.

@chrisccoulson chrisccoulson changed the title env: change how HostEnvironment overrides EFI variables efi: change how HostEnvironment overrides EFI variables Jun 21, 2024
The HostEnvironment interface contains a ReadVar method for abstracting
reads of EFI variables. The default environment just calls
efi.ReadVariable, with other implementations providing mocked variables.

The github.com/canonical/go-efilib package provides a lot more APIs now
that read from (and some that write to) EFI variables and some of these
will be used from the new efi/preinstall package (these APIs are mostly
boot and secure boot related). I wanted a way to be able to override the
environment for the preinstall.RunChecks call (particularly as this is
very useful for testing), but the existing HostEnvironment approach doesn't
work well for this.

I initially considered a couple of options:
- Make it possible to mock each of the individual calls to go-efilib
  functions via the HostEnvironment interface.
- Export varsBackend from go-efilib and provide a function in to mock
  the backend by overriding the system one.

The first approach scales poorly, so my initial approach was going to be
the second one. The problem with this though is that HostEnvironment
will be part of a public, production API, and mocking the backend is global
to the whole process. I don't think this is appropriate in production code
where it might be possible that there may be one goroutine reading EFI
variables from the system when another goroutine decides to mock the backend
based on a supplied HostEnvironment.

I've taken a different approach instead, by modifying every go-efilib
function that interacts with EFI variables to accept a context.Context
argument. The context contains a VarsBackend keyed from it, and the package
exports a DefaultVarContext symbol corresponding to the default system
backend (efivarfs if it is detected, or a null backend if it isn't
detected).

This approach permits individual call sites to override the backend to
use for individual functions that interact with EFI variables (not just
ReadVariable, WriteVariable and ListVariables, but there's a whole set
of extra functions related to secure boot and boot configuration that
make use of reading and writing EFI variables).

The ReadVar method on the HostEnvironment interface has gone, and a new
method that returns a context has been added. The internal.DefaultEnv
implementation just returns efi.DefaultVarContext.

The MockVars type in internal/efitest has been updated to implement
efi.VarsBackend, and the MockHostEnvironment implementation in
internal/efitest returns a context that keys to this implementation.

This should hopefully allow preinstall.RunChecks to be able to take a
HostEnvironment argument and benefit from mocked variables even when
interacting with variables indirectly using higher level functions
exported from go-efilib.
@chrisccoulson chrisccoulson force-pushed the efi-mock-vars-backend-with-hostenv branch from 9474e8f to aaba2c5 Compare June 21, 2024 20:15
@chrisccoulson chrisccoulson force-pushed the efi-mock-vars-backend-with-hostenv branch from aaba2c5 to 12ec80f Compare July 3, 2024 15:26
@chrisccoulson chrisccoulson marked this pull request as ready for review July 3, 2024 15:28
@chrisccoulson chrisccoulson requested a review from pedronis July 3, 2024 15:31
efi/env.go Outdated
// VarContext returns a context containing a VarsBackend, keyed by efi.VarsBackendKey,
// for interacting with EFI variables via go-efilib. This context can be passed to any
// go-efilib function that interacts with EFI variables.
VarContext() context.Context
Copy link
Collaborator

@pedronis pedronis Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit a strange API for contexts, if a caller gets a context it usually should pass it along, and not start with a fresh one, so a more expected API would be:

VartContext(parent context.Context) context.Context

which would return something derived from parent (and panic if parent is nil) instead of returning a fresh context in all cases

otherwise in some scenarios you would end up having to pass more than 1 context.Context to a function which is not how context is usually meant to be used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funnily enough, I'd already added an API for this, so I've just pulled in the go-efilib version with it and used it in the default implementation.

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure the new APIs fully match expectations about context usage

return readVar(name, guid)
// VarContext implements [HostEnvironment.VarContext].
func (e defaultEnvImpl) VarContext() context.Context {
return efi.DefaultVarContext
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this really should really be some form of WithDefaultVarContext from efi

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've updated this now.

@chrisccoulson chrisccoulson force-pushed the efi-mock-vars-backend-with-hostenv branch from 12ec80f to 35d03b3 Compare July 8, 2024 20:37
@chrisccoulson chrisccoulson requested a review from pedronis July 8, 2024 20:39
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the change, couple small remarks

// backend will support this eventually for variable writes because it currently implements
// a retry loop that has a potential to race with other processes. Cancelation and / or
// deadlines for sections of code that performs multiple reads using efivarfs may be useful
// in the future because the kernel rate-limits reads per-user.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the doc say something about parent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the documentation now.

ctx := DefaultEnv.VarContext(context.Background())
c.Assert(ctx, NotNil)

expected := efi.WithDefaultVarsBackend(context.Background())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a good idea to pass something with an attached value already and check it can be retrieved?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this to the test

// backend will support this eventually for variable writes because it currently implements
// a retry loop that has a potential to race with other processes. Cancelation and / or
// deadlines for sections of code that performs multiple reads using efivarfs may be useful
// in the future because the kernel rate-limits reads per-user.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same about parent

@chrisccoulson chrisccoulson requested a review from pedronis July 9, 2024 21:16
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you

@chrisccoulson chrisccoulson merged commit 4b66fa3 into canonical:master Jul 10, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants